package mh.entity;

import static mh.entity.EnumState.BROKEN;
import static mh.entity.EnumState.DAMAGED;
import static mh.entity.EnumState.INTACT;

import java.util.Vector;

import mh.interfaces.Constants;
import mh.map.render.Render;
import mh.util.Pair;

/**
 * Abstract definition for each entity present in the game, whether it is an object, a mob or a player.
 * 
 * @author Gianmarco Laggia
 * @author Alberto D'Este
 */
public abstract class Entity implements IDamageable, Constants {

	/**
	 * Total number of entities spawned since the start of the game. Used to generate an <b>unique</b> ID for each new
	 * <code>Entity</code>
	 */
	private static int			entityCount;

	/** The unique ID of this <code>Entity</code> in this run of the game */
	private final int			entityID;

	// =================================================== //
	//                   POSITION FIELDS                   //
	// =================================================== //
	/** X position of this <code>Entity</code> in the map. Tile units */
	protected double			actualX;

	/** Y position of this <code>Entity</code> in the map. Tile units */
	protected double			actualY;

	/** X position of the Player in the map. Tile units */
	protected double			playerX;

	/** Y position of the Player in the map. Tile units */
	protected double			playerY;

	// =================================================== //
	//                  GAME PLAY FIELDS                   //
	// =================================================== //
	/** Physical weight of this <code>Entity</code>. Used to calculate damage when placed over other objects */
	protected final int			weight;

	/** Damageability of this <code>Entity</code>, normally this should be <code>true</code> */
	protected boolean			damageable;

	/** Minimum amount of damage that can have effect on this <code>Entity</code> */
	protected final int			damageThreshold;

	/** Maximum value of health points, this can depend on level (for players) or other factors */
	protected final int			maximumHealth;

	/** Actual amount of healt points. On creation this value <b>has to be</b> equals to {@link #maximumHealth} */
	protected int				actualHealth;

	/** True if the item can be placed in a stack (on something or untherneath something) */
	private final boolean		stackable;

	/** If present, the <code>Entity</code> that is right over this */
	private Entity				onTop;

	/** The <code>Entity</code> on which this is placed or null if is placed on the floor */
	private Entity				below;

	// =================================================== //
	//                     RENDER FIELDS                   //
	// =================================================== //

	/** The render of this entity */
	protected Render			render;

	/** The states of this entity with their actual frame position */
	protected Vector<EnumState>	actualStates;

	protected boolean			HBVisible	= false;

	/**
	 * Constructor, protected to be used *ONLY* inside a child class.
	 * 
	 * @param damageable
	 *            <code>TRUE</code> if the <code>Entity</code> is damageable
	 * @param damageThreshold
	 *            The minimum amount of damage that should be inflicted to that entity prior to actually cause a loss of
	 *            health
	 * @param maximumHealth
	 *            Max value of health, even when healed the entity cannot pass this limit.
	 * @param stackable
	 *            <code>TRUE</code> if the entity can be put in a stack
	 * @param weight
	 *            Weight of the entity in kg units
	 */
	protected Entity(boolean damageable, int damageThreshold, int maximumHealth, boolean stackable, int weight) {
		this.entityID = entityCount++;
		this.actualX = 0;
		this.actualY = 0;
		this.damageable = damageable;
		this.damageThreshold = damageThreshold;
		this.maximumHealth = maximumHealth;
		this.actualHealth = this.maximumHealth;

		this.actualStates = new Vector<EnumState>();
		this.actualStates.add(EnumState.INTACT);
		//		this.actualStates.add(EnumState.FLOATING);
		//						this.actualStates.add(EnumState.BLASTED);
		//						this.actualStates.add(EnumState.BLASTING_OUT);
		//		this.actualStates.add(EnumState.DAMAGED);
		this.actualStates.add(EnumState.MID_OPEN);
		this.setHealtDirectly((int) (Math.random() * 100), 100);

		this.stackable = stackable;
		this.weight = weight;
		this.onTop = null;
		this.below = null;
		this.render = null;
	}

	/**
	 * @return True if a moving entity can pass over it
	 */
	public abstract boolean isWalkable();

	/**
	 * Sets the health of the entity directly.
	 * 
	 * @param ratio
	 *            The percentual value
	 * @param scale
	 *            The scale we are using that can be 100 or 255
	 */
	public void setHealtDirectly(int ratio, int scale) {
		this.actualHealth = (this.maximumHealth * ratio) / scale;
		if (ratio > 70) {
			this.addState(INTACT);
		} else if (ratio > 0) {
			this.addState(DAMAGED);
		} else {
			this.addState(BROKEN);
		}
	}

	// =================================================== //
	//                  COLLISION HANDLER                  //
	// =================================================== //
	/**
	 * Check if an entity collide with another and handle that specific collision
	 * 
	 * @param with
	 *            The entity to check the collision against
	 * @return True if there is a collision, false otherwise
	 */
	public abstract boolean collide(Entity with);

	// =================================================== //
	//                DAMAGEABILITY & STATES               //
	// =================================================== //
	@Override
	public boolean isDamageable() {
		return this.damageable;
	}

	@Override
	public void setDamageable(boolean dam) {
		this.damageable = dam;
	}

	@Override
	public void damage(Damage damageInflicted) { // TODO return double (reflection) param (Damage damageInflicted)
		while (damageInflicted.hasNext()) {
			Pair<DamageEffectEnum, Integer> dam = damageInflicted.getNext();
			switch (dam.getLeft()) {
				case BLEED:
					break;
				case BLOCK:
					break;
				case DISARM:
					break;
				case DRY:
					break;
				case EXTINGUISH:
					break;
				case BURN:
					break;
				case FROZE:
					break;
				case HEAL:
					break;
				case HIT:
					break;
				case LEVITATE:
					break;
				case PULL:
					break;
				case PUSH:
					break;
				case REPAIR:
					break;
				case SLOWDOWN:
					break;
				case UNBLOCK:
					break;
				case UNFROZE:
					break;
				case WET:
					break;
				case BLAST:
					break;
				case DESTUN:
					break;
				case KILL:
					break;
				case LIGHT:
					break;
				case MAGNET:
					break;
				case STUN:
					break;
				default:
					break;

			}
		}
		//		if (damageAmount <= this.damageThreshold) {
		//			return;
		//		} else {
		//			this.actualHealth -= damageAmount - this.damageThreshold;
		//		}
	}

	@Override
	public void repair(int repairAmount) {
		this.actualHealth += repairAmount;
		if (this.actualHealth > this.maximumHealth) {
			this.actualHealth = this.maximumHealth;
		}
	}

	@Override
	public EnumState getDamageState() {
		for (EnumState s : this.actualStates) {
			if (EnumState.getDamageStates().contains(s)) {
				return s;
			}
		}
		this.actualStates.add(EnumState.INTACT); // this happens only if no damage state is found, but we need always one.
		return EnumState.INTACT;
	}

	@Override
	public double getHealthRatio(int scale) {
		// perc:100 = actual:maximum
		double perc = (this.actualHealth * scale) / this.maximumHealth;
		return perc;
	}

	@Override
	public double getActualHealth() {
		return this.actualHealth;
	}

	@Override
	public double getMaximumHealth() {
		return this.maximumHealth;
	}

	/**
	 * Adds the given state to the state list of this entity.<br>
	 * If the state is a damage state it will remove the already present damage state inside the map.
	 * 
	 * @param s
	 *            The state to add
	 */
	public void addState(EnumState s) {
		if (s.isDamageState()) {
			this.actualStates.remove(this.getDamageState());
		}
		this.actualStates.add(s);
	}

	// =================================================== //
	//                     STACKABILITY                    //
	// =================================================== //
	/**
	 * Place this <code>Entity</code>, if it is stackable, on top of the specified param. If the param is null the
	 * result is that this <code>Entity</code> is placed on the floor.
	 * 
	 * @param ent
	 */
	public void putOn(Entity ent) {
		if (this.stackable) {
			if (ent != null) {
				this.below = ent;
				ent.onTop = this;
			} else {
				this.below = null;
			}
		}
	}

	/**
	 * @return The <code>Entity</code> that is below this or <code>null</code> otherwise
	 */
	public Entity getBelow() {
		return this.below;
	}

	/**
	 * @return The <code>Entity</code> that is on top of this or <code>null</code> otherwise
	 */
	public Entity getOnTop() {
		return this.onTop;
	}

	// =================================================== //
	//                       RENDERER                      //
	// =================================================== //

	/**
	 * @return The actual state list of this <code>Entity</code> and the relative frame position.
	 */
	public Vector<EnumState> getStates() { //HashMap<EnumState, Integer> getStates() {
		return this.actualStates;
		//		return this.actualStates;
	}

	/**
	 * @param s
	 *            The state to look for.
	 * @return True if the Entity contains that specific state.
	 */
	public boolean is(EnumState s) {
		return this.actualStates.contains(s);
	}

	/**
	 * The logic used to draw this <code>Entity</code> in the correct position, with the right state.<br>
	 * Foreground means that this part is drawed on top of Player's Layer to simulate that a player can pass behind the
	 * object.
	 * 
	 * @param delta
	 *            Time passed since last tick
	 * @param foreground
	 *            If True this method will draw the upper part of the entity in the foreground.
	 */
	public abstract void draw(long delta, boolean foreground);

	/**
	 * @return The render (or extension of it) this entity is represented by
	 */
	public Render getRender() {
		return this.render;
	}

	/**
	 * @return True if the health bar is visible
	 */
	public boolean isHealthBarVisible() {
		return this.HBVisible;
	}

	/**
	 * Set the visibility of the health bar
	 * 
	 * @param vis
	 *            True if visible
	 */
	public void setHealthBarVisibility(boolean vis) {
		this.HBVisible = vis;
	}

	// =================================================== //
	//                       GAMEPLAY                      //
	// =================================================== //

	/**
	 * Update the actual position of the MAIN Player.<br>
	 * Doesn't affect the player itself.
	 * 
	 * @param x
	 *            The X position in tile units.
	 * @param y
	 *            The Y position in tile units.
	 */
	public void setPlayerCoord(double x, double y) {
		this.playerX = x;
		this.playerY = y;
	}

	/**
	 * Returns the acutal position (x) of this entity in the map (tile units).
	 * 
	 * @return The X position in tile unit.
	 */
	public double getX() {
		return this.actualX;
	}

	/**
	 * Return the actual position (y) of this entity in the map (tile units).
	 * 
	 * @return The Y position in tile unit.
	 */
	public double getY() {
		return this.actualY;
	}

	/**
	 * Istantly move a Entity to a specified position in the map.
	 * 
	 * @param x
	 *            the X position where to teleport
	 * @param y
	 *            the Y position where to teleport
	 */
	public void teleportTo(double x, double y) {
		this.actualX = x;
		this.actualY = y;
	}

	// =================================================== //
	//                   OBJECT OVERRIDE                   //
	// =================================================== //
	@Override
	public int hashCode() {
		return this.entityID;
	}

	@Override
	public boolean equals(Object obj) {
		if (obj instanceof Entity) {
			if (obj.hashCode() == this.entityID) {
				return true;
			}
		}
		return false;
	}

	//	@Override
	//	public String toString() {
	//		//		return String.format("%s[\'%s\'/%d, x=%.2f, y=%.2f]", new Object[] {this.getClass().getSimpleName(), "name", Integer.valueOf(this.entityID), Double.valueOf(this.actualX), Double.valueOf(this.actualY)});
	//		return ""; //TODO override this method correctly
	//	}
}
